import {
	listGitRefs,
	sparseCheckout,
	listGitFiles,
	resolveCommitHash,
	type GitAdditionalHeaders,
} from './git-sparse-checkout';
import { vi } from 'vitest';

describe('listRefs', () => {
	it('should return the latest commit hash for a given ref', async () => {
		const refs = await listGitRefs(
			'https://github.com/WordPress/wordpress-playground',
			'refs/heads/trunk'
		);
		expect(refs).toEqual({
			'refs/heads/trunk': expect.stringMatching(/^[a-f0-9]{40}$/),
		});
	});
});

describe('resolveCommitHash', () => {
	const repoUrl = 'https://github.com/WordPress/wordpress-playground.git';

	it('infers branch refs when type is omitted', async () => {
		const commitHash = await resolveCommitHash(repoUrl, {
			value: 'trunk',
		});
		expect(commitHash).toMatch(/^[a-f0-9]{40}$/);
	});

	it('infers tag refs without an explicit type', async () => {
		const commitHash = await resolveCommitHash(repoUrl, {
			value: 'v0.1.28',
		});
		expect(commitHash).toMatch(/^[a-f0-9]{40}$/);
	});

	it('resolves explicit tag refs', async () => {
		const commitHash = await resolveCommitHash(repoUrl, {
			value: 'v0.1.28',
			type: 'tag',
		});
		expect(commitHash).toMatch(/^[a-f0-9]{40}$/);
	});

	it('returns the commit hash unchanged when type is commit', async () => {
		const commit = '1234567890abcdef1234567890abcdef12345678';
		const resolved = await resolveCommitHash(repoUrl, {
			value: commit,
			type: 'commit',
		});

		expect(resolved).toBe(commit);
	});

	it('resolves branch refs when type is branch', async () => {
		const resolved = await resolveCommitHash(repoUrl, {
			value: 'trunk',
			type: 'branch',
		});
		expect(resolved).toMatch(/^[a-f0-9]{40}$/);
	});

	it('falls back to tags when branches do not match', async () => {
		const resolved = await resolveCommitHash(repoUrl, {
			value: 'trunk',
		});
		expect(resolved).toMatch(/^[a-f0-9]{40}$/);
	});

	it('throws when the requested branch cannot be found', async () => {
		await expect(
			resolveCommitHash(repoUrl, {
				value: 'missing-branch',
				type: 'branch',
			})
		).rejects.toThrow(
			`Git ref "refs/heads/missing-branch" not found at ${repoUrl}`
		);
	});

	it('throws when neither branch nor tag can be inferred', async () => {
		await expect(
			resolveCommitHash(repoUrl, {
				value: 'missing-ref',
			})
		).rejects.toThrow(`Git ref "missing-ref" not found at ${repoUrl}`);
	});

	it('throws for unsupported ref types', async () => {
		await expect(
			resolveCommitHash(repoUrl, {
				value: 'whatever',
				type: 'unsupported' as any,
			})
		).rejects.toThrow('Invalid ref type: unsupported');
	});
});

describe('sparseCheckout', () => {
	it('should retrieve the requested files and objects from a git repo when withObjects is true', async () => {
		const commitHash = await resolveCommitHash(
			'https://github.com/WordPress/wordpress-playground.git',
			{
				value: 'trunk',
				type: 'branch',
			}
		);
		const result = await sparseCheckout(
			'https://github.com/WordPress/wordpress-playground.git',
			commitHash,
			['README.md'],
			{
				withObjects: true,
			}
		);
		expect(result.files).toEqual({
			'README.md': expect.any(Uint8Array),
		});
		expect(result.files['README.md'].length).toBeGreaterThan(0);
		expect(result.packfiles?.length).toBeGreaterThan(0);
		expect(result.packfiles?.some((packfile) => packfile.promisor)).toBe(
			true
		);
		expect(result.objects?.length).toBeGreaterThan(0);
	});

	it('should retrieve only the requested files from a git repo when withObjects is false', async () => {
		const commitHash = await resolveCommitHash(
			'https://github.com/WordPress/wordpress-playground.git',
			{
				value: 'trunk',
				type: 'branch',
			}
		);
		const result = await sparseCheckout(
			'https://github.com/WordPress/wordpress-playground.git',
			commitHash,
			['README.md'],
			{
				withObjects: false,
			}
		);
		expect(result.files).toEqual({
			'README.md': expect.any(Uint8Array),
		});
		expect(result.files['README.md'].length).toBeGreaterThan(0);
		expect(result.packfiles).toBeUndefined();
		expect(result.objects).toBeUndefined();
	});
});

describe('listGitFiles', () => {
	it('should list the files in a git repo', async () => {
		const commitHash = await resolveCommitHash(
			'https://github.com/WordPress/wordpress-playground.git',
			{
				value: 'trunk',
				type: 'branch',
			}
		);
		const files = await listGitFiles(
			'https://github.com/WordPress/wordpress-playground.git',
			commitHash
		);
		expect(files).toEqual(
			expect.arrayContaining([
				expect.objectContaining({
					name: 'packages',
					type: 'folder',
					children: expect.arrayContaining([
						expect.objectContaining({
							name: 'playground',
							type: 'folder',
							children: expect.arrayContaining([
								expect.objectContaining({
									name: 'storage',
									type: 'folder',
									children: expect.arrayContaining([
										expect.objectContaining({
											name: 'package.json',
											type: 'file',
										}),
									]),
								}),
							]),
						}),
					]),
				}),
			])
		);
	});
});

describe('gitAdditionalHeaders', () => {
	const repoUrl = 'https://github.com/WordPress/wordpress-playground.git';

	it('should successfully fetch when headers is empty object', async () => {
		const headers: GitAdditionalHeaders = {};

		const refs = await listGitRefs(repoUrl, 'refs/heads/trunk', headers);

		expect(refs).toHaveProperty('refs/heads/trunk');
		expect(refs['refs/heads/trunk']).toMatch(/^[a-f0-9]{40}$/);
	});

	it('should pass headers through the full call chain', async () => {
		const headers: GitAdditionalHeaders = {};

		await resolveCommitHash(
			repoUrl,
			{ value: 'trunk', type: 'branch' },
			headers
		);

		expect(headers).toBeDefined();
	});
});

describe('authentication error handling', () => {
	let originalFetch: typeof global.fetch;

	beforeEach(() => {
		originalFetch = global.fetch;
	});

	afterEach(() => {
		global.fetch = originalFetch;
	});

	it('should throw GitAuthenticationError for 401 responses', async () => {
		global.fetch = vi.fn().mockResolvedValue({
			ok: false,
			status: 401,
			statusText: 'Unauthorized',
		});

		const headers: GitAdditionalHeaders = {
			Authorization: 'Bearer token',
		};

		await expect(
			listGitRefs(
				'https://github.com/user/private-repo',
				'refs/heads/main',
				headers
			)
		).rejects.toThrow(
			'Authentication required to access private repository'
		);
	});

	it('should throw GitAuthenticationError for 403 responses', async () => {
		global.fetch = vi.fn().mockResolvedValue({
			ok: false,
			status: 403,
			statusText: 'Forbidden',
		});

		const headers: GitAdditionalHeaders = {
			Authorization: 'Bearer token',
		};

		await expect(
			listGitRefs(
				'https://github.com/user/private-repo',
				'refs/heads/main',
				headers
			)
		).rejects.toThrow(
			'Authentication required to access private repository'
		);
	});

	it('should throw generic error for 404 even with auth token (ambiguous: repo not found OR no access)', async () => {
		global.fetch = vi.fn().mockResolvedValue({
			ok: false,
			status: 404,
			statusText: 'Not Found',
		});

		const headers: GitAdditionalHeaders = {
			Authorization: 'Bearer token',
		};

		await expect(
			listGitRefs(
				'https://github.com/user/repo-or-no-access',
				'refs/heads/main',
				headers
			)
		).rejects.toThrow(
			'Failed to fetch git refs from https://github.com/user/repo-or-no-access: 404 Not Found'
		);
	});

	it('should throw generic error for 404 without auth token', async () => {
		global.fetch = vi.fn().mockResolvedValue({
			ok: false,
			status: 404,
			statusText: 'Not Found',
		});

		await expect(
			listGitRefs(
				'https://github.com/user/nonexistent-repo',
				'refs/heads/main'
			)
		).rejects.toThrow(
			'Failed to fetch git refs from https://github.com/user/nonexistent-repo: 404 Not Found'
		);
	});
});
